组件化架构

背景

模块化

原本一个 app module 承载了所有的功能,而模块化就是拆分成多个模块放在不同的 module 里面。
业务模块
一般的情况下我们会按照 App 的底部 tab 功能还划分 module,以 mashi 为例的话,那就会划分成这样几个业务 module:

通用基础模块
还会有一个通用的基础模块 common_module,提供 BaseActivity/BaseFragment,各个 module 公共的功能等基础能力,每个业务模块都会依赖这个基础模块
基础组件的模块

各个模块之间存在着复杂的依赖关系,多个模块间存在着页面跳转、数据传递、方法调用等情况:room_module 需要 login_module 登录的个人信息;me_module 需要能够在个人主页拨打 1v1 主播的电话,那就需要 ovo_module 提供功能
模块间有着高耦合度问题,如果业务复杂,代码量大的话,严重影响了团队的开发效率及质量,这个时候就需要组件化了。组件化去除模块间的耦合,使得每个业务模块可以独立当做 App 存在,对于其他模块没有直接的依赖关系。 此时业务模块就成为了业务组件

组件化

1、为什么需要组件化?

最早使用的是常见的单工程 MVC 架构,所有业务逻辑都放在了主工程 Module 里,网络层和一些公共代码分别被抽成了一个 Module。后续发展成模块化,但随着业务的快速发展,模块化存在了模块之间大量耦合的问题。
引入了组件化,解决模块化之间的耦合问题

2、组件化的好处?

  1. 加快编译速度和提升开发效率 每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
  2. **业务隔离、提高协作效率 **解耦 使得组件之间 彼此互不打扰,组件内部代码相关性极高。 团队中每个人有自己的责任组件,不会影响其他组件;降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。
  3. **功能重用 **组件 类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。业务组件可上可下,灵活多变;而基础组件,为新业务随时集成提供了基础,减少重复开发和维护工作量。
  4. **代码更简洁 **比如每个 app 都有在网页拉起 app 的动作,未用路由框架时都是写在一个代理的 Activity 中,为其配置 scheme、data 然后转发到目标 Activity,里面要写一堆判断逻辑分发到目标 Activity;用了路由框架代码就很简洁,日后新增的目标 Activity 也基本没有工作量
  5. 更好的实现组件层面的 AOP 组件化之后,我们能很容易地实现一些组件层面的 AOP
    • 轻易实现页面数据 (网络请求、I/O、数据库查询等) 预加载的功能
    • 组件被调用时,进行页面跳转的同时异步执行这些耗时逻辑
    • 页面跳转并初始化完成后,再将这些提前加载好的数据展示出来
    • 在组件功能调用时进行登录状态校验
    • 借助拦截器机制,可以动态给组件功能调用添加不同的中间处理逻辑

3、组件化整体架构

0duor
组件依赖关系是上层依赖下层,修改频率是上层高于下层

  1. 基础组件 通用的基础能力,不包含任何业务逻辑,修改频率低,可作为 SDK 共公司所有项目集成使用,可发布到内网的 maven

常见的基础组件:图片加载、网络服务组件、动态权限、日志、公共 UI 组件、工具类等

  1. **common 组件 **作为支撑业务组件、业务基础组件的基础,同时依赖所有的基础组件,提供多数业务组件需要的基本功能。(所有业务组件、业务基础组件所需的基础能力只需要依赖 common 组件即可)

比如业务组件依赖的公共资源文件,公共网络请求接口,数据 model,组件间通信的下沉接口

  1. **业务基础组件 **对一些系统通用的业务能力进行封装,业务基础组件之间不存在依赖关系,为业务组件提供了一些可复用的基础功能组件;业务基础组件不能单独运行

比如分享能力组件:封装了微信、QQ、微博等分享能力,其他业务只要集成该组件就能进行分享;还有其他的业务基础组件:推送、支付、广告等

  1. **业务组件 **真正的业务组件,通常按照功能模块来划分业务组件;业务组件之间不存在依赖关系,业务组件依赖所需的业务基础组件;业务组件可单独运行;比如注册登录、用户个人中心、APP 首页模块

room 房间、userhome 用户中心、message 消息中心、me 我的页,login 登录页、family 家族、store 商店等

  1. app 壳工程 壳工程依赖了需要集成的业务组件,它可能只有一些配置文件,没有任何代码逻辑。根据你的需要选择集成你的业务组件,不同的业务组件就组成了不同的 APP。

组件之间必须遵守一下规则:

4、组件化开发的问题点

组件化核心点就是去除了组件间的耦合、各个组件间没有了依赖关系。业务组件之间就会有以下问题:

4-1、组件独立调试

  1. gradle.properties 配置变量,通过控制 gradle 的配置,让业务组件可在 library 和 application module 之间切换
  2. module_dependency.gradle 实现,核心就是通过 substitute 来进行转换,依赖的都是 aar,通过 json 文件配置,达到可以切换到源码依赖

具体可参考:组件化下如何优雅进行本地调试,即aar依赖与module依赖动态切换

  1. 自定义插件来实现切换

4-2、组件页面跳转

用 ARouter

比较著名的路由框架 有阿里的 ARouter、美团的 WMRouter,它们原理基本是一致的。TheRouter

4-3、组件间如何通信/方法调用(服务发现)

组件间没有依赖,又如何进行通信呢?服务暴露组件
平时开发中我们常用接口进行解耦,对接口的实现不用关心,避免接口调用与业务逻辑实现紧密关联。这里组件间的解耦也是相同的思路,仅依赖和调用服务接口,不会依赖接口的实现。

  1. 将要通信的接口下沉到 core_module 公共 module 中去
  2. 调用方如何找到提供方的接口实现类?
    1. 在调用方可通过 SPI(Service Provider Interfaces),其实就是找 META-INF/services/ 接口全名配置的接口所有实现类的全路径,通过反射创建类的实现,这样就可以找到了实现、
    2. 提供方通过 ARouter 的 IProvider 暴露服务,提供方添加 @Route 注解,ARouter 通过 apt 就会在指定包中生成对应的代码,在 ARouter 初始化时,会装载到 WareHouse 路由表中去,在调用方使用通过查找路由表,反射创建实例

不足: 下沉到 core module 膨胀怎么优化?

具体见 Android开源库→ARouter

需要暴露服务的 module,通过一个单独的 api module 来提供,其他需要用到该服务的依赖该 module,避免需要下沉到 core module 导致 core module 的膨胀。不需要用的时候也可以直接丢弃掉,这样避免了 core_module 代码膨胀和无关紧要的依赖关系

4-4、Fragment 实例获取

用 ARouter,fragment 添加注解@Route,指定路由路径

4-5、组件间如何初始化

初期版本,没有依赖关系,通过自定义接口的方式

  1. core module 定义一个接口 IAppInit,里面可定义 onCreate、onLowMemory 和 onTrimMemory 等 Application 中的生命周期的函数
  2. 其他需要初始化的 module 新建一个类实现 IAppInit 接口
  3. 在 app module 弄一个配置类,通过反射来创建初始化类
  4. 依赖关系就靠 list 的前后顺序来保证
object AppInitConfig {
    private val appInitList by lazy {
        listOf(
            "me.hacket.mylibrary1.MyLib1AppInit",
            "me.hacket.mylibrary2.MyLib2AppInit",
            "me.hacket.appinitdemo.MainAppInit"
        )
    }
    private val appInitMap by lazy { mutableMapOf<String, IAppInit>() }
    fun onCreate(application: Application) {
        appInitList.forEach {
            try {
                val clazz = Class.forName(it)
                val obj = clazz.newInstance() as? IAppInit
                obj?.let { appInitObj ->
                    appInitMap[it] = appInitObj
                    obj.onCreate(application)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}

SPI 版本,不能处理模块间的依赖关系

通过 SPI(Service Proider interface),将 Application 初始化的逻辑分发到各个 module 中去

object AppInitSPI {

    fun find(application: Application) {
        val serviceLoader = ServiceLoader.load(
            IAppInit::class.java
        )
        for (item in serviceLoader) {
            item.onCreate(application)
        }

        application.registerComponentCallbacks(object : ComponentCallbacks2 {
            override fun onConfigurationChanged(configuration: Configuration) {
                for (item in serviceLoader) {
                    item.onConfigurationChanged(configuration)
                }
            }

            override fun onLowMemory() {
                for (item in serviceLoader) {
                    item.onLowMemory()
                }
            }

            override fun onTrimMemory(level: Int) {
                for (item in serviceLoader) {
                    item.onTrimMemory(level)
                }
            }
        })
    }
}

官方的 StartUp 框架,可处理依赖关系

有依赖关系的初始化

解决:自定义组件间路由框架

如何解决组件间初始化依赖关系?

AppInit

可参考:知乎的Task依赖初始化

组件资源冲突

各组件之间的资源名不能相同,需要配置 resourcePrefix,避免资源重名

## 切图规范
- 只需要放@3x一张图片到drawable-xxhdpi文件即可
- 切图名称:module名称_ic/bg_业务模块名_功能名,如:wisdomsite_ic_home_empty

## 资源规范-string
- 命名:module名称_业务模块名_功能名,如:wisdomsite_labor_title

## 资源规范-color
- colors.xml中定义业务无关的颜色值,colors_feature.xml中使用前者的颜色定义业务相关的颜色值
- colors_feature.xml中的命名:module名称_业务模块名_功能名,如:wisdomsite_machine_error

不同组件资源冲突,如何抉择的?

App module 内资源冲突
 <!--strings.xml-->
<string name="fb_app_id">fb_app_id_lib1</string>
<string name="fb_app_id">fb_app_id_lib1</string>

编译报错:

AGPBI: {"kind":"error","text":"Found item String/fb_app_id more than one time","sources":[{"file":"/Users/xxx/mylibrary1/src/main/res/values/strings.xml"}],"tool":"Resource and asset merger"}
Execution failed for task ':mylibrary1:packageDebugResources'.

/Users/xxx/mylibrary1/src/main/res/values/strings.xml: Error: Found item String/fb_app_id more than one time

 <!--strings.xml-->
 <resources>
 		<string name="fb_app_id">fb_app_id_lib1</string>
 </resources>

 <!--other_strings.xml-->
 <resources>
 		<string name="fb_app_id">fb_app_id_lib1</string>
 </resources>

编译报错:

> AGPBI: {"kind":"error","text":"Duplicate resources","sources":[{"file":{"description":"string/fb_app_id","path":"/Users/xxx/mylibrary1/src/main/res/values/sother_strings.xml"}},{"file":{"description":"string/fb_app_id","path":"/Users/xxx/mylibrary1/src/main/res/values/strings.xml"}}],"tool":"Resource and asset merger"}
> Execution failed for task ':mylibrary1:packageDebugResources'.
>
> > [string/fb_app_id] /Users/xxx/mylibrary1/src/main/res/values/sother_strings.xml	[string/fb_app_id] /Users/xxx/mylibrary1/src/main/res/values/strings.xml: Error: Duplicate resources
Library 和 App module 的资源冲突

Library module 的资源和独立的 App module 资源进行合并。如果双方均具备一个资源 ID 的话,将采用 App 的资源。

Library 之间的资源冲突

如果多个 AAR 库之间发生了冲突,依赖列表里第一个列出(在依赖关系块的顶部)的资源将会被使用。如果 2 个 lib 之间有依赖关系,比如 lib2 依赖 lib1, 那么 lib2 会覆盖 lib1 的同名标识;没有依赖关系,则按照依赖顺序决定

 <!--library1/../strings.xml-->
 <resources>
     <string name="hello">Hello from Library 1!</string>
 </resources>
 
 <!--library2/../strings.xml-->
 <resources>
     <string name="hello">Hello from Library 2!</string>
 </resources>
  1. library1 优先 library2 声明,且 library1 和 library2 没有互相依赖关系
 dependencies {
     implementation project(":library1")
     implementation project(":library2")
     // ...
 }

最后 string/hello 的值将会被编译成 Hello from Library 1!。

  1. 如果这两个 implementation 代码调换顺序,比如 implementation project(":library2") 在前、 implementation project(":library1") 在后,资源值则会被编译成 Hello from Library 2!
  2. app 先后依赖 library2 和 library1,但 library1 依赖 library2,资源值则会被编译成 Hello from Library 1!
冲突解决

不同 module 之间,定义资源的前缀

//resourcePrefix资源前缀限定,只能限定布局文件名和value资源的key,并不能限定图片资源的文件名
android {
    //给电商工程加上前缀约束shopping_
    resourcePrefix "shopping_"
}

android {
    //给直播工程加上前缀约束live_
    resourcePrefix "live_"
}

组件资源冲突检测 Gradle 插件

多仓库 vs 单仓库

组件化过程中遇到的问题?

老项目如何实施组件化?

老项目存在问题

  1. 代码年久失修,文档缺失,不敢随意修改,否则牵一发而动全身,引起现有正常业务的运行
  2. 进行组件化重构需要花费比较长的时间,业务不可能停下来等着你去重构
  3. 组件化重构后,需要全量回归测试,测试比较花费时间

老项目组件化过程

  1. 优先集成路由框架

新开发的功能页面跳转一律采用路由框架,老的页面跳转逐步替换;这样可以尽量减少代码耦合,为后面进行模块拆分打下基础

  1. 抽取出基础功能组件

抽取出来公共的独立的组件,比如日志、网络请求、图片加载、常见的工具类、BaseActivity 等 Base 类

  1. 业务模块拆分

将老项目中的所有业务安装功能进行业务划分,不能太细,否则组件会太多,业务划分一般不要超过 2 层,这样就可以得到一张完整的业务架构图,对 App 所有业务员的模块划分

  1. 抽取基础业务组件

根据 3 画出来的业务架构图,将一个个组件抽离出来。这个过程会出现各种依赖问题,如果是两个组件间的以依赖问题,我们需要用路由框架的组件间通信的功能来将这两个组件间的依赖给去除掉

  1. 新老代码共存

老项目的组件化需要一定的时间,这个过程中,新开发的功能与重构并行的进行。经过一段时间逐步将老工程的业务全部组件化

组件源码和 aar

组件的源码和 aar 依赖关系的如何切换?

  1. 通过 gradle 脚本,配合在 gradle.properties 配置变量来控制源码和 aar 之间的切换
  2. 通过自定义插件来实现(吸音就是这样)

组件间 aar 和源码依赖传递 merge 失败?

组件间源码依赖和 aar 依赖传递导致的 merge 失败问题,通过 substitute 强制转换为同一种

allprojects { p ->
    /*
     * 修复构建的依赖传递导致merge失败, 如:
     * qsbk.app.remix
     *   ↳ :live (local and changed) <-----------┒
     *   ↳ qsbk.app:feed:x.y.z (remote)          ┆
     *     ↳ qsbk.app:live:x.y.z (remote) <------┚
     */
    configurations.all {
        resolutionStrategy.dependencySubstitution {
            def libs = ['core', 'libcommon', 'libwidget']
            libs.each {
                if (libsInSource.toBoolean()) {
//                    println("----------- substitute module(${Config.Maven.groupId + ":${it}:" + Config.moduleVersion[it]}) with project(:${it})")
                    substitute module(Config.Maven.groupId + ":${it}:" + Config.moduleVersion[it]) with project(":${it}")
                }
            }
        }
    }
}

组件的 aar 如何更新?

通过 jenkins ci 工具,启一个定时 Job,每天早上将有更新的组件的代码编译成功后 push 到 maven 仓库

众多组件,可能几十个,如何拉取,更新的问题,版本如何维护?

多仓库问题

  1. 切换分支麻烦,拉取慢
  2. 分支容易对不齐,打包失败

组件化过程中常见小问题

组件中 ButterKnife 报错—R2

在 Library 中,ButterKnife 注解中使用 R.id 会报错,这是因为在 library 中生成的 R 文件,这些属性值都不是常量
解决:通过 ButterKnife 提供的 Gradle 插件,引用 R2 来解决该问题

路由框架如何抉择?

架构设计→路由框架设计.md

ARouter 遇到的问题

  1. 一个 group 不能在多个 module 中出现
  2. 注入空问题,声明我非空,但调用方没有带该字段,还是有可能为空,

ARouter 有什么不足?

具体见 Android开源库→ARouter

  1. 路由表扫描和注册在启动时扫描 dex?

用三方插件可编译器通过 transform+ASM 装载路由表避免在启动时扫描 dex 浪费性能

  1. 接口下沉到 core_module 导致该 module 膨胀?

服务暴露方提供单独的 xxx_export 暴露组件供需要方依赖,避免下沉到 core_module

  1. 一个页面只支持一个 path,不支持多个 path,也不支持正则表达式
  2. startActivityForResult 后需要重写 onActivityResult() 方法,导致路由和结果的代码分散难以维护
  3. 缺少组件间初始化,需要支持组件间初始化依赖关系
  4. 拦截器是全局的,所有路由时都会走拦截器,不支持对某个目标页面指定特定的拦截器
  5. 跨进程调佣支持不咋地

组件化思考

1、路由和总线?

市面上的路由框架基本都是基于路由的。

相同点

路由和组件总线都需要将分布在不同组件 module 中的某些类按照一定规则生成映射表(通常是 Map,key 为字符串,Value 为类或对象),然后在需要用到的时候从映射表中根据字符串取出类或对象

不同点

  1. 路由方案
  1. 组件总线方案

2、路由框架通信时下沉接口导致 core module 膨胀,怎么解决?

  1. 两个 module 之间通信的接口不下沉到 core module,而是由服务提供的 module 新建一个 module_export module 专门用来通信用的,调用方只需要依赖这个 module 即可,就可以避免 core module 接口膨胀问题
  2. 服务发现采用组件总线方案,而不是路由方案

如何设计一个路由框架?